package org.bultreebank.index.schema.management.impl;


import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.bultreebank.document_repository.impl.IndexesDocumentsImpl;
import org.bultreebank.index.exceptions.FileNotCreatedException;
import org.bultreebank.index.schema.management.interfaces.IndexingSchemaManager;
import org.bultreebank.index.utils.Constants;
import org.bultreebank.index.utils.Pair;
import org.bultreebank.index.utils.Utils;
import org.bultreebank.index.utils.Validator;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import clark.dtd.DTD;
import clark.xpath.MainXPath;

/**
 * Default Implementation of the IndexingSchemaInterface
 * @author Aneliya Tincheva
 *
 */
public class IndexingSchemaImpl implements IndexingSchemaManager, Constants{

	public final String INDEXES_FILE_NAME = "index_names";
	public final String INDEX_DTD_FILE_NAME = "index_schema.dtd";
	public final String INDEX_DTD_DOCTYPE = "schema";
	
	public final String FIELD = "field";
	private final String XPATH_FIELDS = "//" + FIELD;
	private final String XPATH_FIELDS_CONTEXTS = "ancestor::context";
	private final String LEFT_SLASH = "/";
	private final String XPATH = "xpath";
	private final String DOC = "doc";
	private final String SUBINDEX_ID = "realIndexID";
	private final String INTERNAL_INDEX_ID = "indexID";
	private final String VALUE = "value";
	private final String INDEXES_REPOSITORY = "IndexRepository";
	private DTD indexSchemaDTD;
	private Properties systemIndexIDs_realIndexIDs;
	private Properties realIndexIDs_systemIndexIDs;
	private String indexRepositoryPath;
	private String searchEnginePath;

	public IndexingSchemaImpl(String searchEnginePath) throws Exception
	{
		if (searchEnginePath == null || 
				searchEnginePath.equals(EMPTY_STR))
			throw new Exception("The path is not specified!");
		Utils.createNewFolder(searchEnginePath);
		this.searchEnginePath = searchEnginePath;
		this.indexRepositoryPath = searchEnginePath + 
		SEPARATOR + INDEXES_REPOSITORY; 
		Utils.createNewFolder(indexRepositoryPath); 
		systemIndexIDs_realIndexIDs = new Properties();
		realIndexIDs_systemIndexIDs = new Properties();
		loadIndexesFile();
		
	}
	
	/**
	 * Returns the index repository path registered
	 * @return
	 */
	public String getIndexRepositoryPath()
	{
		return indexRepositoryPath;
	}
	
	/**
	 * Returns the search engine repository path
	 * @return
	 */
	public String getSearchEngineRepositoryPath()
	{
		return searchEnginePath;
	}
	
	/**
	 * Returns a Properties object, representing the 
	 * system generated - user defined index IDs
	 * @return
	 */
	public Properties getAllSystem_UserIndexes() {
		return systemIndexIDs_realIndexIDs;
	}

	/**
	 * Returns a Properties object, representing the 
	 * user defined - system generated index IDs
	 * @return
	 */
	public Properties getAllUser_SystemIndexes() {
		return realIndexIDs_systemIndexIDs;
	}
 
	/**
	 * Returns the system generated index ID for the specified user defined
	 * index ID
	 * @param userIndexID
	 * @return
	 * @throws Exception
	 */
	public String getInternalIndexID(String userIndexID) throws Exception {
		
		return realIndexIDs_systemIndexIDs.getProperty(userIndexID);
	}

	/**
	 * Returns the user defined index ID corresponding to the specified 
	 * internal index ID
	 * @param internalIndexID
	 * @return
	 * @throws Exception
	 */
	public String getUserIndexID(String internalIndexID) throws Exception {
		
		return systemIndexIDs_realIndexIDs.getProperty(internalIndexID);
	}
	
	/**
	 * Returns true if the specified index exists in the index repository 
	 * @param realIndexID - user defined index ID
	 * @return
	 * @throws Exception
	 */
	public boolean ifIndexExists(String indexID) throws Exception {
		if (realIndexIDs_systemIndexIDs.get(indexID) == null)
			throw new Exception ("There is no index schema registered for the index!");
		String existingIndex = realIndexIDs_systemIndexIDs.get(indexID).
			toString(); 
		if (existingIndex == null || existingIndex.equals(EMPTY_STR) )
			return false;
		return true;
	}

	/**
	 * Normalizes and adds the normalized index document to 
	 * the index repository.
	 * @param indexSchema - user defined indexing schema 
	 * @return
	 * @throws Exception
	 */
	public String addIndexSchema(Document indexSchema) throws Exception
	{
	
		if (!Validator.isIndexSchemaValid(searchEnginePath, indexSchema, 
				INDEX_DTD_DOCTYPE))
			throw new Exception("The index schema document is not valid!");
		Element schemaRoot = indexSchema.getDocumentElement();
		if (schemaRoot.getChildNodes().getLength() == 0)
			throw new Exception("The indexing schema document is Invalid!\n There are no field or context elements!");
		String xpath = "//context[not(descendant::context) and not(descendant::field)]";
		List<Node> notProperContexts = MainXPath.processXPath(xpath,schemaRoot);
		if (!notProperContexts.isEmpty())
			throw new Exception("The indexing schema document is Invalid!\n It is not allowed to have context elements with empty element value!");
		Pair<String, String> entry = generateInternalIndexID(schemaRoot);
		String internalIndexID = entry.getFirst();
		if (systemIndexIDs_realIndexIDs.containsKey(internalIndexID))
			throw new Exception("Such an index exists. Provide a new index identifier in the indexing schema!");
		
		String path = indexRepositoryPath + SEPARATOR +
			internalIndexID;
		Utils.createNewFolder(path);
		normalizeIndexSchema(indexSchema, path, entry.getFirst());
		if (!systemIndexIDs_realIndexIDs.contains(internalIndexID))
		{
			addEntry(entry);
			path = path + SEPARATOR + internalIndexID;
			Utils.createNewFile(path);
			Utils.saveFile(path , Utils.getStringRepresentationOfDocument(indexSchema));
		}
		
		return entry.getSecond();
	}

	/**
	 * Updates (created new normalized document and saves it in the Index 
	 * Repository) the index schema document for the specified index
	 * @param indexSchemaDoc - user defined index schema document
	 * @param realIndexID
	 * @throws Exception
	 */
	public void updateIndexSchema(Document indexSchemaDoc,
			String realIndexID) throws Exception
	{
		String internalIndexID = realIndexIDs_systemIndexIDs.getProperty(realIndexID);
		if (internalIndexID == null || internalIndexID.equalsIgnoreCase(EMPTY_STR))
			throw new Exception ("There is no index: " + realIndexID + "!");
		String path = indexRepositoryPath + SEPARATOR + internalIndexID + 
			SEPARATOR + internalIndexID;
		File f = new File(path);
		if (!f.exists())
			throw new Exception ("There is no index schema for the index " 
				+ realIndexID + " !");
		Utils.saveDocument(path, null, indexSchemaDoc);
		
	}
	
	/**
	 * Returns the normalized indexing schema document relevant
	 * for the specified index
	 * @param realIndexID - the user defined index ID
	 * @return
	 * @throws Exception
	 */
	public Document getIndexSchema(String realIndexID) throws Exception
	{
		String internalIndexID = realIndexIDs_systemIndexIDs.getProperty(realIndexID);
		if (internalIndexID == null || internalIndexID.equalsIgnoreCase(EMPTY_STR))
			throw new Exception ("There is no index: " + realIndexID + "!");
		String path = indexRepositoryPath + SEPARATOR + internalIndexID + 
			SEPARATOR + internalIndexID;
		File f = new File(path);
		if (!f.exists())
			throw new Exception ("There is no index schema for the index " 
				+ realIndexID + " !");
		return Utils.getDocumentFromString(
				Utils.getStringRepresentationOfFileContent(path));
	}

	/**
	 * Returns true if there are no documents added to the index and false 
	 * otherwise
	 * @param realIndexID - user defined index ID
	 * @return
	 * @throws Exception
	 */
	public boolean isIndexEmpty(String realIndexID) throws Exception
	{
		String internalIndexID = realIndexIDs_systemIndexIDs.
			getProperty(realIndexID);
		Map<String, List<String>> map = IndexesDocumentsImpl.
		loadIndexesDocs(searchEnginePath);
		if (!map.containsKey(internalIndexID))
			return true;
		List<String> docs = map.get(internalIndexID);
		if (docs != null && !docs.isEmpty())
			return false;
		return false;
	}
	
	/**
	 * 
	 * @param realIndexID - user defined indexing schema document 
	 * @param restrictive - boolean variable used for specifying the deletion
	 * mode (true = if there are any documents added to the specified index 
	 * it wouldn't be deleted; false = otherwise)
	 * @return
	 * @throws Exception
	 */
	public void removeIndexSchema(String realIndexID,
			boolean restrictive) throws Exception{
		String internalIndexID = realIndexIDs_systemIndexIDs.getProperty(realIndexID);
		if (internalIndexID == null || internalIndexID.equalsIgnoreCase(EMPTY_STR))
			throw new Exception ("There is no index: " + realIndexID + "!");
		if (!isIndexEmpty(realIndexID)&& restrictive)
			throw new Exception ("There are documents in the index: " + 
					realIndexID + ". It wouldn't be deleted!");
		Pair<String, String> entry = new Pair(internalIndexID, realIndexID);
		File f = new File(indexRepositoryPath);
		File [] internalFiles = f.listFiles();
		for (File file: internalFiles)
		{
			if (file.getName().equals(internalIndexID))
			{
				
				boolean deleted = Utils.deleteFileOrDirectory(file.getAbsolutePath());//file.delete();
				if (!deleted)
					throw new Exception ("The deletion of index: " + realIndexID + 
							" is  NOT successful!");
				deleteEntry(entry);
			}
		}
	}

	private Pair<String, String> generateInternalIndexID(Element root) throws Exception
	{
		String realIndexID = root.getAttribute(NAME_ATTR);
		if (realIndexIDs_systemIndexIDs.contains(realIndexID))
			throw new Exception("The index schema " + realIndexID +
					" is already added!");
		String value = Utils.getAvailableInternalID(INDEX_FIELDS_FILE_NAME,
				realIndexID,
				realIndexIDs_systemIndexIDs);
		Pair<String, String> entry = new Pair(value, realIndexID);
		return entry;
	}
	
	private void normalizeIndexSchema(Document indexSchema, 
			String indexPath, String value)
	throws Exception
	{
		int cNum = 0;
		Element root = indexSchema.getDocumentElement();
		root.setAttribute(INTERNAL_INDEX_ID_ATTR, value);
		List<Node> rootContexts = MainXPath.processXPath(
				"//context[not(parent::context)]",
				root);
		List<Node> fields = MainXPath.processXPath(
				XPATH_FIELDS,
				root);
		List<Node> normalizedContexts = new ArrayList<Node>();
		for (Node obj:fields)
		{
			Element field = (Element)obj;
			String valueAttr = field.getAttribute(VALUE);
			
			if (valueAttr != null && !valueAttr.equalsIgnoreCase(EMPTY_STR))
			{
				List<Node> contextParents = MainXPath.processXPath(
						XPATH_FIELDS_CONTEXTS,
						field);
				
				if (contextParents != null && !contextParents.isEmpty())
				{
					StringBuffer xpath = new StringBuffer(LEFT_SLASH);
					StringBuffer contextID = new StringBuffer(DOC);
					for (int i = contextParents.size() - 1; i >= 0; i--)
					{
						
						Element context =  (Element) contextParents.get(i);
						xpath.append(LEFT_SLASH_CHAR + context.getAttribute(
								XPATH));
						contextID.append(COLUMN + context.getAttribute(NAME));
						if (( context.getParentNode().getNodeName().equals(CONTEXT) || field.getParentNode().equals(context))
								&& !normalizedContexts.contains(context))
						{
							normalizedContexts.add(context);
							String contextIDAttr = "c" + cNum;
							cNum++;
							String absolutePath = context.getAttribute(ABSOLUTE_XPATH);
							context.setAttribute(SUBINDEX_ID, contextID.toString());
							context.setAttribute(INTERNAL_INDEX_ID, contextIDAttr);//contextIDString);
							if (rootContexts.contains(context) && (absolutePath == null || absolutePath.equals(EMPTY_STR)))
							{
								
								
								context.setAttribute(ABSOLUTE_XPATH,
									xpath.toString());
							}
						}
				
					}
				}
			
			}
		}
	}
	
	private void loadIndexesFile()
	throws FileNotCreatedException,IOException
	{
		String path = indexRepositoryPath + SEPARATOR + 
			INDEXES_FILE_NAME;
		Utils.loadTableFile(systemIndexIDs_realIndexIDs,
				realIndexIDs_systemIndexIDs, path);
	}
	private void saveIndexesFile() throws Exception
	{
		String path = indexRepositoryPath + SEPARATOR +
			INDEXES_FILE_NAME;
		Utils.saveTableFile(path, systemIndexIDs_realIndexIDs);
	}
	
	private void addEntry(Pair<String, String> entry) throws Exception
	{
		systemIndexIDs_realIndexIDs.put(entry.getFirst(), entry.getSecond());
		realIndexIDs_systemIndexIDs.put(entry.getSecond(), entry.getFirst());
		saveIndexesFile();
	}
	private void deleteEntry(Pair<String, String> entry) throws Exception
	{
		systemIndexIDs_realIndexIDs.remove(entry.getFirst());
		realIndexIDs_systemIndexIDs.remove(entry.getSecond());
		saveIndexesFile();
	}

	@Override
	public String getLang(String indexID) throws Exception
	{
		Document indexSchema = getIndexSchema(indexID);
		return indexSchema.getDocumentElement().getAttribute(LANG);
	}
}
